enum JMDateExtensionFormatType {
  ymdHms,
  ymdHm,
  ymd,
  ym,
  mdHms,
  mdHm,
  md,
  hms,
  hm,
  ms,
}

/// Extension on the [DateTime]
extension JMDateExtension on DateTime {
  /// JMDateExtension: Returns a [DateTime] for each day the given range.
  /// * [start] inclusive
  /// * [end] exclusive
  ///
  /// Example:
  /// ```dart
  /// DateTime.daysInRange(DateTime.now(), DateTime.now().addDays(10))
  /// ```
  static Iterable<DateTime> daysInRange(DateTime start, DateTime end) sync* {
    var i = start;
    var offset = start.timeZoneOffset;
    while (i.isBefore(end)) {
      yield i;
      i = i.addDays(1);
      var timeZoneDiff = i.timeZoneOffset - offset;
      if (timeZoneDiff.inSeconds != 0) {
        offset = i.timeZoneOffset;
        i = i.subtract(Duration(seconds: timeZoneDiff.inSeconds));
      }
    }
  }

  /// JMDateExtension: Add a certain amount of days to this date.
  /// * [amount] add days
  ///
  /// Example:
  /// ```dart
  /// DateTime.now().addDays(10)
  /// ```
  DateTime addDays(int amount) {
    return DateTime(
      year,
      month,
      day + amount,
      hour,
      minute,
      second,
      millisecond,
      microsecond,
    );
  }

  /// JMDateExtension: Add a certain amount of hours to this date.
  /// * [amount] add hours
  ///
  /// Example:
  /// ```dart
  /// DateTime.now().addHours(10)
  /// ```
  DateTime addHours(int amount) {
    return DateTime(
      year,
      month,
      day,
      hour + amount,
      minute,
      second,
      millisecond,
      microsecond,
    );
  }

  /// JMDateExtension: Date is it today.
  ///
  /// Example:
  /// ```dart
  /// DateTime.now().isToday
  /// ```
  bool get isToday {
    final nowDate = DateTime.now();
    return year == nowDate.year && month == nowDate.month && day == nowDate.day;
  }

  /// JMDateExtension: Date is it yesterday.
  ///
  /// Example:
  /// ```dart
  /// DateTime.now().isYesterday
  /// ```
  bool get isYesterday {
    final nowDate = DateTime.now();
    return year == nowDate.year && month == nowDate.month && day == nowDate.day - 1;
  }

  /// JMDateExtension: Date is it tomorrow.
  ///
  /// Example:
  /// ```dart
  /// DateTime.now().isTomorrow
  /// ```
  bool get isTomorrow {
    final nowDate = DateTime.now();
    return year == nowDate.year && month == nowDate.month && day == nowDate.day + 1;
  }

  bool get isLeapYear => (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));

  /// JMDateExtension: The day after this [DateTime]
  DateTime get nextDay => addDays(1);

  /// JMDateExtension: The day previous this [DateTime]
  DateTime get previousDay => addDays(-1);

  /// JMDateExtension: Whether or not two times are on the same day.
  bool isSameDay(DateTime b) => (year == b.year && month == b.month && day == b.day);

  /// JMDateExtension: The list of days in a given month.
  List<DateTime> get daysInMonth {
    var first = firstDayOfMonth;
    var daysBefore = first.weekday;
    var firstToDisplay = first.subtract(Duration(days: daysBefore));
    var last = lastDayOfMonth;

    var daysAfter = 7 - last.weekday;

    // If the last day is sunday (7) the entire week must be rendered
    if (daysAfter == 0) {
      daysAfter = 7;
    }

    var lastToDisplay = last.add(Duration(days: daysAfter));
    return daysInRange(firstToDisplay, lastToDisplay).toList();
  }

  /// JMDateExtension: First day of month.
  DateTime get firstDayOfMonth => DateTime(year, month);

  /// JMDateExtension: First day of week.
  DateTime get firstDayOfWeek {
    /// Handle Daylight Savings by setting hour to 12:00 Noon
    /// rather than the default of Midnight
    final day = DateTime.utc(year, month, this.day, 12);

    /// Weekday is on a 1-7 scale Monday - Sunday,
    /// This Calendar works from Sunday - Monday
    var decreaseNum = day.weekday % 7;
    return subtract(Duration(days: decreaseNum));
  }

  /// JMDateExtension: Last day of week.
  DateTime get lastDayOfWeek {
    /// Handle Daylight Savings by setting hour to 12:00 Noon
    /// rather than the default of Midnight
    final day = DateTime.utc(year, month, this.day, 12);

    /// Weekday is on a 1-7 scale Monday - Sunday,
    /// This Calendar's Week starts on Sunday
    var increaseNum = day.weekday % 7;
    return day.add(Duration(days: 7 - increaseNum));
  }

  /// JMDateExtension: Last day of month.
  DateTime get lastDayOfMonth {
    var beginningNextMonth = (month < 12) ? DateTime(year, month + 1, 1) : DateTime(year + 1, 1, 1);
    return beginningNextMonth.subtract(const Duration(days: 1));
  }

  /// JMDateExtension: Previous month for this month.
  DateTime get previousMonth {
    var year = this.year;
    var month = this.month;
    if (month == 1) {
      year--;
      month = 12;
    } else {
      month--;
    }
    return DateTime(year, month);
  }

  /// JMDateExtension: Next month for this month.
  DateTime get nextMonth {
    var year = this.year;
    var month = this.month;

    if (month == 12) {
      year++;
      month = 1;
    } else {
      month++;
    }
    return DateTime(year, month);
  }

  /// JMDateExtension: Previous week for this week.
  DateTime get previousWeek => subtract(const Duration(days: 7));

  /// JMDateExtension: Next week for this week.
  DateTime get nextWeek => add(const Duration(days: 7));

  /// JMDateExtension: Is same week of this week with give date.
  bool isSameWeek(DateTime b) {
    /// Handle Daylight Savings by setting hour to 12:00 Noon
    /// rather than the default of Midnight
    final a = DateTime.utc(year, month, day);
    b = DateTime.utc(b.year, b.month, b.day);

    final diff = a.toUtc().difference(b.toUtc()).inDays;
    if (diff.abs() >= 7) {
      return false;
    }

    final min = a.isBefore(b) ? a : b;
    final max = a.isBefore(b) ? b : a;
    final result = max.weekday % 7 - min.weekday % 7 >= 0;
    return result;
  }

  /// JMDateExtension: DateTime to String.
  ///
  /// Example:
  /// ```dart
  /// DateTime.now().toDateString()
  /// ```
  String toDateString({
    JMDateExtensionFormatType format = JMDateExtensionFormatType.ymdHms,
    String remark = '-',
  }) {
    return _dateStringFormat(format, remark: remark);
  }

  String _dateStringFormat(JMDateExtensionFormatType type, {required String remark}) {
    var year = this.year;
    var month = this.month.toString().padLeft(2, '0');
    var day = this.day.toString().padLeft(2, '0');
    var hour = this.hour.toString().padLeft(2, '0');
    var minute = this.minute.toString().padLeft(2, '0');
    var second = this.second.toString().padLeft(2, '0');

    switch (type) {
      case JMDateExtensionFormatType.ymdHms:
        return '$year$remark$month$remark$day $hour:$minute:$second';
      case JMDateExtensionFormatType.ymdHm:
        return '$year$remark$month$remark$day $hour:$minute';
      case JMDateExtensionFormatType.ymd:
        return '$year$remark$month$remark$day';
      case JMDateExtensionFormatType.ym:
        return '$year$remark$month';
      case JMDateExtensionFormatType.mdHms:
        return '$month$remark$day $hour:$minute:$second';
      case JMDateExtensionFormatType.mdHm:
        return '$month$remark$day $hour:$minute';
      case JMDateExtensionFormatType.md:
        return '$month$remark$day';
      case JMDateExtensionFormatType.hms:
        return '$hour:$minute:$second';
      case JMDateExtensionFormatType.ms:
        return '$minute:$second';
      case JMDateExtensionFormatType.hm:
        return '$hour:$minute';
      default:
        return '';
    }
  }
}
